﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Lidgren.Network;
using System.Reflection;
using Kaleta.Network.Serialization;

namespace Kaleta.Network
{
    /// <summary>
    /// This is abstract class, which instances of are distributed over network. 
    /// In a network session there are unique objects of type DistributableObject.
    /// </summary>
    public abstract class DistributableObject
    {
        /// <summary>
        /// True if this object is already in server or created by server. 
        /// Otherwise false.
        /// </summary>
        public bool IsDistributed { get; set; }
        public int Key { get; set; }

        /// <summary>
        /// Specifies what client created this object. Can be used to identify local objects.
        /// </summary>
        public int ClientOwnerID { get; set; }

        /// <summary>
        /// This is internal property.
        /// </summary>
        internal bool Distribute { get; set; }

        public DistributableObject()
        {
            this.Distribute = true;

            //this is temporary, it will be re-set on the DistributionSession.Creation:
            this.Key = DateTime.Now.GetHashCode();
        }



        public override bool Equals(object obj)
        {
            DistributableObject dobj = obj as DistributableObject;

            if (dobj == null) return false;

            return this.Key == dobj.Key;
        }

        public override int GetHashCode()
        {
            return this.Key;
        }

        /// <summary>
        /// Specifies wheter this object was created locally or has been created remotely by other client.
        /// Don't use it in type's constructor. The value is initialized after Distributing (via CreateObject or DistributeObject).
        /// </summary>
        public bool IsCreatedLocally
        {
            get
            {
                return DistributionSession.ClientID == this.ClientOwnerID;
            }
        }
    }

    /// <summary>
    /// Represets a property, which value is dependent on server update.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class DistributableProperty<T> where T : struct
    {
        /// <summary>
        /// Synchronized value
        /// </summary>
        private T value;

        /// <summary>
        /// This is a helper variable to avoid sending the same value to server
        /// </summary>
        private T sentValue;

        private DistributableObject owner;
        private PropertyInfo propertyInfo;
        private NetChannel channel;

        public delegate void PropertyChangedCallback(object sender, PropertyChangedEventArgs<T> args);
        private PropertyChangedCallback propertyChangedCallback = null;

        private DistributableProperty(DistributableObject owner, PropertyInfo propInfo, NetChannel channel, PropertyChangedCallback callback)
        {
            this.owner = owner;
            this.propertyInfo = propInfo;
            this.channel = channel;
            this.propertyChangedCallback = callback;
        }


        /// <summary>
        /// If client (this class) sets a new value for this property, the client requests the server for update of this value.
        /// After invoking of this method, the client will still use old value until it gets from server new updated value.
        /// Returns true if update was from server. If update is from client returns false.
        /// </summary>
        /// <param name="newValue"></param>
        public bool SetValue(T newValue, out T oldValue)
        {
            //if (!this.owner.IsDistributed)
            //    throw new DistributableObjectException("Object is not distributed yet. Prior of using Setter, use CreateObject() or DistributeObject()");

            //set from client:
            if (this.owner.Distribute)
            {
                if (!newValue.Equals(sentValue))
                {
                    DistributionSerializer.Serialize(DistributionSession.clientSendBuffer[(int)this.channel],
                            DistributionType.Update, this.owner, this.propertyInfo, newValue, this.channel);

                    oldValue = default(T);
                    sentValue = newValue;
                    return false;
                }

            }
            //set from server:
            oldValue = this.value;
            this.value = newValue;

            if (propertyChangedCallback != null)
                propertyChangedCallback(this.owner, new PropertyChangedEventArgs<T>(newValue, oldValue));

            return true;
        }

        /// <summary>
        /// If client (this class) sets a new value for this property, the client requests the server for update of this value.
        /// After invoking of this method, the client will still use old value until it gets from server new updated value.
        /// Returns true if update was from server. If update is from client returns false.
        /// </summary>
        /// <param name="newValue"></param>
        public bool SetValue(T newValue)
        {
            //if (!this.owner.IsDistributed)
            //    throw new DistributableObjectException("Object is not distributed yet. Prior of using Setter, use CreateObject() or DistributeObject()");

            //set from client:
            if (this.owner.Distribute)
            {
                if (!newValue.Equals(sentValue))
                {
                    DistributionSerializer.Serialize(DistributionSession.clientSendBuffer[(int)this.channel],
                            DistributionType.Update, this.owner, this.propertyInfo, newValue, this.channel);

                    sentValue = newValue;
                    return false;
                }

            }
            //set from server:
            var oldValue = this.value;
            this.value = newValue;

            if (propertyChangedCallback != null)
                propertyChangedCallback(this.owner, new PropertyChangedEventArgs<T>(newValue, oldValue));

            return true;
        }

        /// <summary>
        /// Gets value for this property. If server was not yet updated, old value will be returned. 
        /// If server has been updated (and notified the client), new value will be returned.
        /// </summary>
        /// <returns>New or old value depending on server notification(update)</returns>
        public T GetValue()
        {
            //if (!this.owner.IsCreated)
            //    throw new DistributableObjectException("The object has not been distributed yet. Use CreateObject() or DistrubuteObject before using a Setter/Getter of this property.");

            return this.value;
        }

        /// <summary>
        /// Registers property for network distribution.
        /// </summary>
        /// <param name="propertyName">Name of property to register</param>
        /// <param name="typeOfClass">Type of property to register. Can only be of type struct. No reference objects supported.</param>
        /// <returns></returns>
        static public DistributableProperty<T> Register(string propertyName, DistributableObject owner, NetChannel channel)
        {
            PropertyInfo propertyInfo = owner.GetType().GetProperties().SingleOrDefault(property => property.Name.Equals(propertyName));
            if (propertyInfo == null)
                throw new DistributableObjectException("The type '" + owner.GetType().FullName + "' does not have property '" + propertyName + "'");

            return new DistributableProperty<T>(owner, propertyInfo, channel, null);
        }


        /// <summary>
        /// Registers property for network distribution.
        /// Channel is unreliable.
        /// </summary>
        /// <param name="propertyName">Name of property to register</param>
        /// <param name="typeOfClass">Type of property to register. Can only be of type struct. No reference objects supported.</param>
        /// <returns></returns>
        static public DistributableProperty<T> Register(string propertyName, DistributableObject owner)
        {
            PropertyInfo propertyInfo = owner.GetType().GetProperties().Single(property => property.Name.Equals(propertyName));

            return new DistributableProperty<T>(owner, propertyInfo, NetChannel.Unreliable, null);
        }

        /// <summary>
        /// Registers property for network distribution with a callback if the value gets updated (propagated from server).
        /// </summary>
        /// <param name="propertyName">Name of property to register</param>
        /// <param name="typeOfClass">Type of property to register. Can only be of type struct. No reference objects supported.</param>
        /// <returns></returns>
        static public DistributableProperty<T> Register(string propertyName, DistributableObject owner, NetChannel channel, PropertyChangedCallback callback)
        {
            PropertyInfo propertyInfo = owner.GetType().GetProperties().Single(property => property.Name.Equals(propertyName));

            return new DistributableProperty<T>(owner, propertyInfo, channel, callback);
        }

        /// <summary>
        /// Registers property for network distribution with a callback if the value gets updated (propagated from server).
        /// Channel is unreliable.
        /// </summary>
        /// <param name="propertyName">Name of property to register</param>
        /// <param name="typeOfClass">Type of property to register. Can only be of type struct. No reference objects supported.</param>
        /// <returns></returns>
        static public DistributableProperty<T> Register(string propertyName, DistributableObject owner, PropertyChangedCallback callback)
        {
            PropertyInfo propertyInfo = owner.GetType().GetProperties().Single(property => property.Name.Equals(propertyName));

            return new DistributableProperty<T>(owner, propertyInfo, NetChannel.Unreliable, callback);
        }
    }



    public class DistributableObjectException : Exception
    {
        public DistributableObjectException(string message) : base(message) { }

    }

    public class PropertyChangedEventArgs<T> : EventArgs
    {
        public T OldValue { get; private set; }
        public T NewValue { get; private set; }

        public PropertyChangedEventArgs(T newValue, T oldValue)
        {
            OldValue = oldValue;
            NewValue = newValue;
        }
    }
}
